Skip to content

Comments

fix(uptime): Defensive error handling in deletion cascade for billing seats#108554

Merged
dashed merged 5 commits intomasterfrom
bil-2126/fix-uptime-seat-cleanup-on-project-deletion
Feb 20, 2026
Merged

fix(uptime): Defensive error handling in deletion cascade for billing seats#108554
dashed merged 5 commits intomasterfrom
bil-2126/fix-uptime-seat-cleanup-on-project-deletion

Conversation

@dashed
Copy link
Member

@dashed dashed commented Feb 19, 2026

Summary

Fixes BIL-2126: Uptime monitor billing seats not cleared when deleting a project.

The root cause is that the post_delete signal handler for uptime seat removal (in getsentry) has no error handling. When any call inside it fails, the exception propagates through Detector.delete(), breaks the DB transaction, and crashes the entire scheduled deletion task. The project gets stuck in PENDING_DELETION and billing seats remain orphaned.

This PR adds two layers of defense in the sentry deletion framework:

Fix 1: UptimeSubscriptionDeletionTask error handling

  • Wraps get_detector() in try/except Detector.DoesNotExist
  • If the detector is already deleted or the DataSource chain is broken, the deletion proceeds without crashing
  • Also fixes a typo in an upstream comment ("billing east" → "billing seat")

Fix 2: DetectorDeletionTask proactive seat removal

  • Adds a delete_instance() override that calls remove_uptime_seat() before the detector is deleted
  • Belt-and-suspenders: even if the cascade from Detector → DataSource → UptimeSubscription breaks, the seat is still removed here
  • Any exception in remove_uptime_seat() is caught and logged, so it never blocks the deletion

Tests

  • test_delete_uptime_detector_calls_remove_seat — verifies remove_seat is called during deletion
  • test_delete_uptime_detector_succeeds_when_remove_seat_fails — detector deletion proceeds when remove_seat raises
  • test_delete_uptime_subscription_without_detector — UptimeSubscription deletion works when detector is missing
  • Updated test_delete_with_uptime_monitors to verify belt-and-suspenders (2 calls to remove_seat)

Related

  • Companion getsentry PR (signal handler error handling + cleanup job re-enablement): pending
  • See also: getsentry#19371 (different approach, adds Project-level signal handler)

Test plan

  • All new tests pass locally
  • CI passes
  • Existing deletion tests still pass

@linear
Copy link

linear bot commented Feb 19, 2026

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Feb 19, 2026
@dashed dashed marked this pull request as ready for review February 20, 2026 19:34
@dashed dashed requested a review from a team as a code owner February 20, 2026 19:34
@dashed dashed requested a review from a team February 20, 2026 19:34
Copy link
Member

@krithikravi krithikravi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, we can't exactly reproduce the issue from prod, but this looks like it offers a couple of defenses against possible causes

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

When an UptimeSubscription is deleted as part of the cascade from
project deletion, get_detector() can raise Detector.DoesNotExist if
the detector was already deleted or the DataSource relationship is
broken. This would crash the entire deletion and leave billing seats
orphaned.

Handle the DoesNotExist case gracefully by logging a warning and
continuing with the subscription deletion.

Ref: BIL-2126
Add belt-and-suspenders seat removal directly in DetectorDeletionTask.
Previously, uptime seat removal only happened through the cascade
(Detector -> DataSource -> UptimeSubscription -> remove_uptime_seat) or
via the post_delete signal in getsentry. If either path failed, the
seat would be orphaned.

Now the DetectorDeletionTask proactively removes the seat before
deleting the detector, with error handling to ensure deletion
proceeds even if seat removal fails.

Ref: BIL-2126
Add tests covering the new error handling in detector and uptime
subscription deletion tasks:

- test_delete_uptime_detector_calls_remove_seat: verifies remove_seat
  is called when an uptime detector is deleted via DetectorDeletionTask
- test_delete_uptime_detector_succeeds_when_remove_seat_fails: verifies
  detector deletion proceeds even if remove_uptime_seat raises
- test_delete_uptime_subscription_without_detector: verifies
  UptimeSubscription deletion proceeds when the detector no longer exists

Update test_delete_with_uptime_monitors to account for remove_seat
now being called from both DetectorDeletionTask and
UptimeSubscriptionDeletionTask (belt-and-suspenders).

Ref: BIL-2126
The DetectorDeletionTask.delete_instance() now calls remove_uptime_seat()
before super().delete_instance(), which calls instance.delete() and sets
instance.pk=None. Since Python's mock stores references (not copies),
the mock's last recorded call shows id=None after the object is mutated.

Switch to assert_any_call which verifies the expected call was made at
any point, avoiding the reference mutation issue.
@dashed dashed force-pushed the bil-2126/fix-uptime-seat-cleanup-on-project-deletion branch from 463e4d3 to 0f90038 Compare February 20, 2026 21:17
… test

The test_delete_uptime_detector_succeeds_when_remove_seat_fails test
was not actually exercising the error path it claimed to test.
UptimeSubscriptionDeletionTask binds remove_uptime_seat via a
top-level import, so patching the source module alone left that call
unaffected. Only DetectorDeletionTask's lazy import picked up the
mock, consuming the success side_effect and never triggering the
exception.

Mock at both import locations so the exception is raised in
DetectorDeletionTask and add assert_called_once to verify the error
path is exercised.
@dashed dashed force-pushed the bil-2126/fix-uptime-seat-cleanup-on-project-deletion branch from 0f90038 to 4c90381 Compare February 20, 2026 21:24
@dashed dashed merged commit 8673c3e into master Feb 20, 2026
80 checks passed
@dashed dashed deleted the bil-2126/fix-uptime-seat-cleanup-on-project-deletion branch February 20, 2026 22:13
priscilawebdev pushed a commit that referenced this pull request Feb 24, 2026
… seats (#108554)

## Summary

Fixes
[BIL-2126](https://linear.app/getsentry/issue/BIL-2126/uptime-monitor-seats-not-cleared-when-deleting-project):
Uptime monitor billing seats not cleared when deleting a project.

The root cause is that the `post_delete` signal handler for uptime seat
removal (in getsentry) has no error handling. When any call inside it
fails, the exception propagates through `Detector.delete()`, breaks the
DB transaction, and crashes the entire scheduled deletion task. The
project gets stuck in `PENDING_DELETION` and billing seats remain
orphaned.

This PR adds **two layers of defense** in the sentry deletion framework:

### Fix 1: `UptimeSubscriptionDeletionTask` error handling
- Wraps `get_detector()` in `try/except Detector.DoesNotExist`
- If the detector is already deleted or the DataSource chain is broken,
the deletion proceeds without crashing
- Also fixes a typo in an upstream comment ("billing east" → "billing
seat")

### Fix 2: `DetectorDeletionTask` proactive seat removal
- Adds a `delete_instance()` override that calls `remove_uptime_seat()`
**before** the detector is deleted
- Belt-and-suspenders: even if the cascade from Detector → DataSource →
UptimeSubscription breaks, the seat is still removed here
- Any exception in `remove_uptime_seat()` is caught and logged, so it
never blocks the deletion

### Tests
- `test_delete_uptime_detector_calls_remove_seat` — verifies
`remove_seat` is called during deletion
- `test_delete_uptime_detector_succeeds_when_remove_seat_fails` —
detector deletion proceeds when `remove_seat` raises
- `test_delete_uptime_subscription_without_detector` —
UptimeSubscription deletion works when detector is missing
- Updated `test_delete_with_uptime_monitors` to verify
belt-and-suspenders (2 calls to `remove_seat`)

## Related
- Companion getsentry PR (signal handler error handling + cleanup job
re-enablement): pending
- See also:
[getsentry#19371](getsentry/getsentry#19371)
(different approach, adds Project-level signal handler)

## Test plan
- [x] All new tests pass locally
- [ ] CI passes
- [ ] Existing deletion tests still pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code-assisted Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants